package p39_boot执行流程;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.boot.*;
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogs;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.*;
import org.springframework.context.support.GenericApplicationContext;

import org.springframework.core.env.CommandLinePropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.SimpleCommandLinePropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.ResourcePropertySource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import p21_参数解析器.Person;

import java.lang.reflect.Constructor;
import java.time.Duration;
import java.util.List;


/**
 * SpringApplication构造之后，调用该对象的run方法，并返回一个容器对象，一个boot程序启动成功
 * 下面模拟了run方法的12个步骤
 */
@Slf4j
public class BootRunner {
    public static void main(String[] args) throws Exception {
        SpringApplication application = new SpringApplication();
        //添加初始化器和监听器(上一节介绍过)
        application.addInitializers(context -> System.out.println("===>执行初始化器..."));
        application.addListeners(e -> log.info(e.getClass().getName()));
        /**
         * 第一步:获取事件发布器,用于容器启动过程发布事件
         *      事件发布器负责在容器的启动过程中的各个节点发送对应的事件，这些事件会被addListeners注册的事件监听器接收到
         *      启动过程中共有七大事件
         */
        //SpringFactoriesLoader，是加载并获取SpringFactories的工具类，可以根据接口类(key)获取所有实现类的名字(values)，两个参数:接口类class，类加载器
        //spring.factories是springboot自动装配必要的文件,SpringApplicationRunListener是这个文件中事件发布器的类名，根据这个接口可以获取文件中的实现类的全类名
        List<String> names = SpringFactoriesLoader.loadFactoryNames(SpringApplicationRunListener.class, BootRunner.class.getClassLoader());
        //下面演示了如何用事件发布器发送事件，在boot容器run方法的不同地方，会发布事件，这里仅是演示
        for (String name : names) {
            System.out.println(name);
            //创建事件发布器实现类的对象
            Class<?> clazz = Class.forName(name);
            Constructor<?> constructor = clazz.getConstructor(SpringApplication.class, String[].class);
            //这个一个事件发布器的实现类
            SpringApplicationRunListener publisher = (SpringApplicationRunListener)constructor.newInstance(application, args);
            DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
            GenericApplicationContext context = new GenericApplicationContext();

            /**
             * 准备发送事件，七大事件
             * 下面发送的事件，会被上面给application添加的事件监听器监听到
             */
            //spring boot开始启动发送的事件
            publisher.starting(bootstrapContext);

            //环境(配置文件)准备完毕发送的事件
            publisher.environmentPrepared(bootstrapContext, new StandardEnvironment());

            //spring容器创建并调用初始化器之后，发送的事件
            publisher.contextPrepared(context);

            //所有bean definition加载完毕
            publisher.contextLoaded(context);

            context.refresh();

            //spring容器调用refresh之后，发送的事件
            publisher.started(context, Duration.ZERO);

            //spring boot启动完毕发送的事件
            publisher.ready(context, Duration.ZERO);

            //启动过程中，出现错误，发送的事件
            publisher.failed(context, new Exception("错误啦..."));
        }

        /**
         * 第二步:封装启动args参数，作为环境变量、以及供后面回调runner时使用
         * 参数有两个类型:
         *  1.选项参数：--key=value
         *  2.原始参数：debug
         */
        DefaultApplicationArguments arguments = new DefaultApplicationArguments(args);

        /**
         * 3~6步都是跟环境有关，也就是系统环境变量，可能来自操作系统，也可能来自配置文件，也可能来自启动参数
         * 第三步:从系统的环境变量，启动项命令，和配置文件中配置的变量，整合到一起，统一封装到ApplicationEnvironment
         *   boot是封装到ApplicationEnvironment，保护类，外部无法访问
         *   spring是封装到StandardEnvironment
         * 注意：这一步还没有开始添加boot的配置文件application.properties(yml/yaml)来源，下面仅仅是为了演示
         */
        StandardEnvironment standardEnvironment = new StandardEnvironment();
        //手动添加配置文件来源，来源添加到最后面，表示优先级最低。得到环境变量时会在前面开始的来源里找，如果前面找到了就不找了，否则会一直往后找
        standardEnvironment.getPropertySources().addLast(new ResourcePropertySource(new ClassPathResource("p28/application.yml")));
        //命令行源，优先级最高，来自命令行参数
        standardEnvironment.getPropertySources().addFirst(new SimpleCommandLinePropertySource());
        for (PropertySource<?> ps : standardEnvironment.getPropertySources()) {
            //四个源，分别是jvm虚拟机参数(systemProperties)、操作系统环境变量(systemEnvironment)，以及我们手动添加的两个源
            System.out.println(ps);
        }

        /**
         * 第四步:在第三步的基础上，加了一个特殊源，使得将各个变量的不规范命名变成规范
         * 比如配置文件中key的命名可能以"-","_"分割，也可能以小驼峰命名，讲这些命名统一变成"-"分割的
         * 下面代码将这个特殊源(configurationProperty)加在standardEnvironment的最前面
         */
        ConfigurationPropertySources.attach(standardEnvironment);

        /**
         * 第五步:对ApplicationEnvironment做了进一步增强，增了一些额外的源
         * 发送事件，事件通过使用Environment后处理器完成的增强,不同位置的application.properties(yml/yaml)源就是在这一步被解析添加的
         * Environment后处理器的实现类定义在自动配置配置文件中，SpringFactoriesLoader来读取
         * 下面演示的所有Environment后处理器，实际都来自spring.factories配置文件，第五步实际是读取这个文件，加载并执行所有EnvironmentPostProcessor接口的实现
         * 同样也是通过SpringFactoriesLoader.loadFactoryNames(EnvironmentPostProcessor.class, classLoader)进行的获取
         * 那是谁读取这个配置文件，得到并回调所有的EnvironmentPostProcessor实现类的postProcessEnvironment方法呢?
         *  - 这个操作是通过监听器执行的(EnvironmentPostProcessorApplicationListener)，监听器是构造过程中添加的，监听器同样是通过spring.factories获取的
         *  - 第五步实际会发送事件(environmentPrepared事件)，该事件就会被EnvironmentPostProcessorApplicationListener监听到
         */
        EnvironmentPostProcessor environmentPostProcessor = new ConfigDataEnvironmentPostProcessor(new DeferredLogs(), new DefaultBootstrapContext());
        //ConfigDataEnvironmentPostProcessor能解析各个位置下的application.properties(yml/yaml)，作为源添加到standardEnvironment。当然也有其他具有不同功能的实现
        environmentPostProcessor.postProcessEnvironment(standardEnvironment, application);

        /**
         * 第六步：数据绑定
         * 从ApplicationEnvironment找到合适的键(前缀为spring.main的键)，与SpringApplication的属性进行绑定
         * @ConfigurationProperties注解的原理(绑定过程类似)
         */
        //从环境变量中得到所有前缀为person的数据，绑定到Person对象的属性上
        Person person = Binder.get(standardEnvironment).bind("person", Person.class).get();
        //绑定到SpringApplication
        Binder.get(standardEnvironment).bind("spring.main", Bindable.ofInstance(application));

        /**
         * 第七步:设置寻找并打印banner的实现类
         * SpringApplicationBannerPrinter类的访问级别是默认，这里无法得到
         */

        /**
         * 第八步:创建容器，假设是servlet类型
         * 根据构造过程中推断的应用类型，创建不同的容器实现
         */
        GenericApplicationContext context = createApplicationContext(WebApplicationType.SERVLET);

        /**
         * 第九步:准备容器
         * 回调在构造过程中，准备的初始化器
         */
        for (ApplicationContextInitializer initializer : application.getInitializers()) {
            initializer.initialize(context);
        }

        /**
         * 第十步:加载bean定义(根据构造时得到的BeanDefinition源)
         * AnnotatedBeanDefinitionReader读取配置类的bean定义，将bean定义存在指定beanFactory
         * 可以加载各个来源的bean，不同来源有不同工具类
         * 除了这里补充的bean定义，初始化容器时，bean工厂后处理器(初始化时添加的，或自动装配的)也可以解析一些注解并放入bean定义到容器(应该吧)
         */
        //加载配置类源
        DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
        AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(beanFactory);
        reader.register(Config.class);
        //加载配置文件源
        XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(beanFactory);
        xmlReader.loadBeanDefinitions(new ClassPathResource("p02/TestPathXml.xml"));
        //以扫描包作为源
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(beanFactory);
        scanner.scan("p39_boot执行流程.bean");

        /**
         * 第十一步:refresh容器
         * 初始化容器，根据bean definition创建单例，回调bean工厂后处理器,准备bean后处理器
         */
        context.refresh();
        for (String name : context.getBeanDefinitionNames()) {
            System.out.println(name + "--->" + context.getBeanFactory().getBeanDefinition(name).getResourceDescription());
        }

        /**
         * 第十二步:执行runner
         * 从容器中得到所有CommandLineRunner和ApplicationRunner的实现类，并回调其方法
         * 此时容器已经refresh了，故而可以正常使用spring容器
         */
        for (CommandLineRunner runner : context.getBeansOfType(CommandLineRunner.class).values()) {
            runner.run(args);
        }
        for (ApplicationRunner runner : context.getBeansOfType(ApplicationRunner.class).values()) {
            //参数是前面封装的启动args
            runner.run(arguments);
        }

        context.close();

    }

    //第八步:创建容器
    private static GenericApplicationContext createApplicationContext(WebApplicationType type) {
        GenericApplicationContext context = null;
        //根据推断的类型(构造环节推断的)，获取对应的容器对象
        switch (type) {
            case SERVLET -> context = new AnnotationConfigServletWebServerApplicationContext();
            case REACTIVE -> context = new AnnotationConfigReactiveWebServerApplicationContext();
            case NONE -> context = new AnnotationConfigApplicationContext();
        }
        return context;
    }

    @Configuration
    static class Config {
        @Bean
        public String bean1() {
            return "hahah";
        }

        @Bean
        public ServletWebServerFactory servletWebServerFactory() {
            return new TomcatServletWebServerFactory();
        }

        @Bean
        public CommandLineRunner commandLineRunner() {
            return args -> System.out.println("commandLineRunner.....");
        }

        @Bean
        public ApplicationRunner applicationRunner() {
            return args -> System.out.println("applicationRunner.....");
        }
    }

}
